/*******************************************************************************
 * Copyright (c) 2008 Conselleria de Infraestructuras y Transporte, Generalitat
 * de la Comunitat Valenciana . All rights reserved. This program
 * and the accompanying materials are made available under the terms of the
 * Eclipse Public License 2.0 which accompanies this distribution, and is
t https://www.eclipse.org/legal/epl-2.0/
t
t SPDX-License-Identifier: EPL-2.0
 *
 * Contributors: Francisco Javier Cano Muñoz (Prodevelop) – Initial implementation.
 *
 ******************************************************************************/
package org.eclipse.papyrus.uml.diagram.common.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.Platform;
import org.eclipse.papyrus.uml.diagram.common.Activator;

/**
 * A basic simple IExtensionPoint parser. It parses an IExtensionPoint and
 * stores values in instances of the specified Classes specified. It uses java
 * reflection to get the elements and their attributes to parse. It can handle
 * String, Boolean and Object attributes. It can handle any IExtensionPoint as
 * long as the proper Classes are provided. <br>
 * It needs the IExtensionPoint string ID. It needs an Array of Classes to
 * create instances of that kind. If the Classes are internal to another Class,
 * an instance of the enclosing Class is required. <br>
 * The Classes provided must follow these rules:
 * <ul>
 * <li>Each Class simple name must be equal to one of the elements defined in the IExtensionPoint to be parsed.</li>
 * <li>There must be one Class for each one of the elements defined in the IExtensionPoint.</li>
 * <li>Each Class must have one attribute of type String, Boolean or Object for each attribute defined for its matching element in the IExtensionPoint.</li>
 * <li>Each Class must have one attribute of type List for each type of child element it can have as defined in the IExtensionPoint. The name of this List attribute will be equal to the type of elements to be stored in it.</li>
 * <li>Use Boolean instead of boolean, this allows that field to be null, indicating it has not been initialized.</li>
 * </ul>
 * <br>
 * The method parseExtensionPoint will return a List of Objects with a data structure similar to the
 * one defined in the IExtensionPoint.
 *
 * @author <a href="mailto:fjcano@prodevelop.es">Francisco Javier Cano Muñoz</a>
 *
 */
public class ExtensionPointParser {

	/**
	 * <Class>es from which create instances.
	 */
	private Class<Object>[] classes = null;

	/**
	 * Enclosing instance for internal <Class>es.
	 */
	private Object enclosingInstance = null;

	/**
	 * <IExtensionPoint> full name identifier.
	 */
	private String extensionPointID = null;

	/**
	 * Constructor without enclosing <Class>.
	 *
	 * @param extensionPointID
	 * @param classes
	 */
	public ExtensionPointParser(String extensionPointID, Class<Object>[] classes) {
		this.classes = classes;
		this.extensionPointID = extensionPointID;
	}

	/**
	 * Constructor with enclosing <Class>.
	 *
	 * @param extensionPointID
	 * @param classes
	 * @param enclosingInstance
	 */
	// @unused
	public ExtensionPointParser(String extensionPointID, Class<Object>[] classes, Object enclosingInstance) {
		this.classes = classes;
		this.extensionPointID = extensionPointID;
		this.enclosingInstance = enclosingInstance;
	}

	public Class<Object>[] getClasses() {
		return classes;
	}

	public String getExtensionPointID() {
		return extensionPointID;
	}

	public Class getEnclosingClass() {
		if (getEnclosingInstance() != null) {
			return getEnclosingInstance().getClass();
		}
		return null;
	}

	public Object getEnclosingInstance() {
		return enclosingInstance;
	}

	/**
	 * Parses the <IExtensionPoint> specified in the constructor. Returns a
	 * <List> of <Object> instances created from the <Class>es provided in the
	 * constructor. Fields with null value in the returned <Object>s have not
	 * been initialized, as those values were not specified in the
	 * <IExtensionPoint>.
	 *
	 * @return
	 */
	public List<Object> parseExtensionPoint() {
		if (getExtensionPointID() == null || getClasses() == null || getClasses().length <= 0) {
			return Collections.EMPTY_LIST;
		}

		IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry();
		IExtensionPoint extensionPoint = extensionRegistry.getExtensionPoint(getExtensionPointID());

		// In case the extension point does not exist, show an error
		if (extensionPoint == null) {
			Activator.getDefault().logError("The Extension Point ID provided does not exist", null);
			return Collections.EMPTY_LIST;
		}

		// parse every root IConfigurationElement in this IExtensionPoint.
		List<Object> objects = new ArrayList<Object>();
		for (IConfigurationElement element : extensionPoint.getConfigurationElements()) {
			Object object = parseConfigurationElement(element);
			if (object != null) {
				objects.add(object);
			}
		}

		return objects;
	}

	/**
	 * Parse an <IConfigurationElement>, including its attributes and its
	 * children elements,
	 *
	 * @param element
	 * @return
	 */
	protected Object parseConfigurationElement(IConfigurationElement element) {
		if (element == null) {
			return null;
		}

		Object object = createInstance(element);
		if (object == null) {
			return null;
		}

		// Added by gmerin to store the pluginID that contributed this object
		if (object instanceof IObjectWithContributorId) {
			String pluginID = element.getContributor().getName();
			((IObjectWithContributorId) object).setContributorId(pluginID);
		}
		// end gmerin

		for (Field field : object.getClass().getDeclaredFields()) {
			if (field.getType().isAssignableFrom(List.class) && !field.getType().isAssignableFrom(Object.class)) {
				// List fields are handled later
				continue;
			}
			String attribute = field.getName();
			if (attribute == null || attribute.contains("$")) {
				// a field with "$" is a pointer to the enclosing instance; and
				// never accessible
				continue;
			}
			Object value = parseAttribute(element, attribute);
			if (!(value instanceof String)) {
				if (field.getType().isAssignableFrom(List.class)) {
					try {
						if (field.get(object) == null) {
							// if a list without value, set to EMPTY_LIST
							field.set(object, Collections.EMPTY_LIST);
						}
					} catch (IllegalArgumentException e) {
						error("Cannot acces field " + attribute, e);
					} catch (IllegalAccessException e) {
						error("Cannot acces field " + attribute, e);
					}
				}
				continue;
			}
			String valueString = (String) value;
			try {
				if (field.getType().isAssignableFrom(Object.class)) {
					// an Object field is instantiated
					Object instantiation = null;
					try {
						instantiation = element.createExecutableExtension(attribute);
						field.set(object, instantiation);
					} catch (CoreException e) {
						error("Cannot create instance of " + valueString, e);
						instantiation = null;
					}
				} else if (field.getType().isAssignableFrom(Boolean.class)) {
					// a Boolean field is parsed to its primitive value.
					field.set(object, Boolean.valueOf(valueString));
				} else {
					field.set(object, valueString);
				}
			} catch (IllegalAccessException ex) {
				error("Cannot acces field " + attribute, ex);
				return null;
			} catch (IllegalArgumentException ex) {
				error("Illegal value for " + attribute, ex);
				return null;
			}
		}
		// parse all children element
		List<Object> objects = new ArrayList<Object>();
		for (IConfigurationElement child : element.getChildren()) {
			Object parsedChild = parseConfigurationElement(child);
			if (parsedChild != null) {
				objects.add(parsedChild);
			}
		}
		// add those children element to the proper <List> attribute
		addChildrenToInstance(object, objects);
		// object ready!
		return object;
	}

	/**
	 * Gets an attribute from an <IconfigurationElement>.
	 *
	 * @param element
	 * @param attribute
	 * @return
	 */
	protected Object parseAttribute(IConfigurationElement element, String attribute) {
		return element.getAttribute(attribute);
	}

	/**
	 * Adds children instances to the proper <List> attribute of the instance
	 * parent.
	 *
	 * @param instance
	 * @param children
	 */
	protected void addChildrenToInstance(Object instance, List<Object> children) {
		for (Object child : children) {
			String name = child.getClass().getSimpleName();
			try {
				Field field = instance.getClass().getField(name);
				Object value = field.get(instance);
				if (field.getType().isAssignableFrom(List.class)) {
					if (value == null) {
						value = new ArrayList<Object>();
						field.set(instance, value);
					}
					((List) value).add(child);
				}
			} catch (NoSuchFieldException ex) {
				error("No field named " + name, ex);
				return;
			} catch (IllegalAccessException ex) {
				error("Field " + name + " not accessible", ex);
				return;
			}
		}
	}

	/**
	 * Creates an instance of an <Object> searching for the matching <Class> by
	 * simple name. The list of <Class>es to be used are provided in the
	 * constructor. Internal <Class>es must also provide an enclosing instance
	 * in the constructor.
	 *
	 * @param element
	 * @return
	 */
	protected Object createInstance(IConfigurationElement element) {
		String name = element.getName();
		for (Class<Object> clazz : getClasses()) {
			if (clazz.getSimpleName().equals(name)) {
				try {
					Class[] parameters = null;
					if (getEnclosingClass() != null) {
						parameters = new Class[] { getEnclosingClass() };
					} else {
						parameters = new Class[0];
					}
					Constructor<Object> constructor = clazz.getConstructor(parameters);
					Object[] arguments = null;
					if (getEnclosingInstance() != null) {
						arguments = new Object[] { getEnclosingInstance() };
					} else {
						arguments = new Object[0];
					}
					return constructor.newInstance(arguments);
				} catch (IllegalAccessException ex) {
					error("Cannot access constructor for " + name, ex);
					return null;
				} catch (IllegalArgumentException ex) {
					error("Cannot create instance of type " + name, ex);
					return null;
				} catch (InstantiationException ex) {
					error("Cannot create instance of type " + name, ex);
					return null;
				} catch (NoSuchMethodException ex) {
					error("Cannot find constructor for " + name, ex);
					return null;
				} catch (InvocationTargetException ex) {
					error("Cannot invoke constructor for " + name, ex);
					return null;
				}
			}
		}
		return null;
	}

	private void error(String message, Throwable throwable) {
		Activator.getDefault().logError(message, throwable);
	}

}
